package transport;

import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Observable;
import java.util.Observer;

import network.Network;
import network.NetworkPacket;
import network.TPA;

public class Transport extends Thread implements Observer {

	ArrayList<TransportConnection> connections;
	ArrayList<ListeningConnection> listeners;
	Network network;
	TransportRoutingTable routingTable;
	ServerSocket serverSocket;
	int lastListened;
	Socket s;

	public Transport(Network net) {
		connections = new ArrayList<TransportConnection>();
		listeners = new ArrayList<ListeningConnection>();
		network = net;
		routingTable = new TransportRoutingTable();
		lastListened = 0;
		try {
			serverSocket = new ServerSocket(5555, 0,
					InetAddress.getByName("localhost"));
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	/**
	 * Sends an ack to the destined TPA without data. Is sent when data has been
	 * received, but no data is waiting to be sent.
	 * 
	 * @param connection
	 *            the connection from which the packet is sent, includes a
	 *            sequence number and the ports
	 * @param to
	 *            the TPA that the packet is sent to
	 * @throws IOException
	 */
	public void ack(TransportConnection connection, TPA to) throws IOException {
		TransportPacket toSend = new TransportPacket(
				(byte) connection.getInPort(), (byte) connection.getOutPort(),
				(char) connection.getSeq(), (char) connection.getAckSeq(),
				true, false, false, new byte[0]);
		sendPacket(toSend, to, connection);
		// System.out.println("Ik ack op seqnummer: " + (int)
		// toSend.getAckseq());
	}

	/**
	 * Sends an ack to the destined TPA with additional data. Is sent when data
	 * has been received and the other side wants to send data too.
	 * 
	 * @param connection
	 *            the connection from which the packet is sent, includes a
	 *            sequence number and the ports
	 * @param to
	 *            the TPA that the packet is sent to
	 * @throws IOException
	 */
	public void ack(TransportConnection connection, TPA to, byte[] data, int seq)
			throws IOException {
		TransportPacket toSend = new TransportPacket(
				(byte) connection.getInPort(), (byte) connection.getOutPort(),
				(char) seq, (char) connection.getAckSeq(), true, false, false,
				data);
		sendPacket(toSend, to, connection);
		// System.out.println("Ik ack op seqnummer: " + (int)
		// toSend.getAckseq());
	}

	/**
	 * Sends a request for connection to the destined TPA. is sent when a
	 * connection has to be established. This will cause the connection to wait
	 * for a SYNACK
	 * 
	 * @param connection
	 *            the connection from which the packet is sent, includes a
	 *            sequence number and the ports
	 * @param to
	 *            the TPA that the packet is sent to
	 * @throws IOException
	 */
	public void connect(TransportConnection connection, TPA to)
			throws IOException {
		TransportPacket toSend = new TransportPacket(
				(byte) connection.getInPort(), (byte) connection.getOutPort(),
				(char) connection.getSeq(), (char) connection.getAckSeq(),
				false, true, false, new byte[0]);
		sendPacket(toSend, to, connection);
	}

	/**
	 * Sends an ack to the destined TPA, confirming a connection has been made.
	 * This will establish the connection and allow data to be transported.
	 * 
	 * @param connection
	 *            the connection from which the packet is sent, includes a
	 *            sequence number and the ports
	 * @param to
	 *            the TPA that the packet is sent to
	 * @throws IOException
	 */
	public void connectack(TransportConnection connection, TPA to)
			throws IOException {
		TransportPacket toSend = new TransportPacket(
				(byte) connection.getInPort(), (byte) connection.getOutPort(),
				(char) connection.getSeq(), (char) connection.getAckSeq(),
				true, true, false, new byte[0]);
		sendPacket(toSend, to, connection);
	}

	/**
	 * Sends a fin bit to the destined TPA, signaling that the connection should
	 * be shutdown. Causes the connection to wait for a FINACK.
	 * 
	 * @param connection
	 *            the connection from which the packet is sent, includes a
	 *            sequence number and the ports
	 * @param to
	 *            the TPA that the packet is sent to
	 * @throws IOException
	 */
	public void quit(TransportConnection connection, TPA to) throws IOException {
		TransportPacket toSend = new TransportPacket(
				(byte) connection.getInPort(), (byte) connection.getOutPort(),
				(char) connection.getSeq(), (char) connection.getAckSeq(),
				false, false, true, new byte[0]);
		sendPacket(toSend, to, connection);
	}

	/**
	 * Sends an ack to the destined TPA. This confirms the connection has
	 * shutdown.
	 * 
	 * @param connection
	 *            the connection from which the packet is sent, includes a
	 *            sequence number and the ports
	 * @param to
	 *            the TPA that the packet is sent to
	 * @throws IOException
	 */
	public void quitack(TransportConnection connection, TPA to)
			throws IOException {
		TransportPacket toSend = new TransportPacket(
				(byte) connection.getInPort(), (byte) connection.getOutPort(),
				(char) connection.getSeq(), (char) connection.getAckSeq(),
				true, false, true, new byte[0]);
		sendPacket(toSend, to, connection);
	}

	/**
	 * Sends data without any other connectioncontrol bits set.
	 * 
	 * @param connection
	 *            the connection from which the packet is sent. includes all
	 *            information needed to construct the packet.
	 * @param to
	 *            the TPA to send to
	 * @throws IOException
	 */
	public void data(TransportConnection connection, TPA to, byte[] data,
			int seq) throws IOException {
		TransportPacket toSend = new TransportPacket(
				(byte) connection.getInPort(), (byte) connection.getOutPort(),
				(char) seq, (char) connection.getAckSeq(), false, false, false,
				data);
		sendPacket(toSend, to, connection);
		// System.out.println("Ik stuur data, met seqnummer: " +
		// (int)toSend.getSeq());
	}

	/**
	 * Requests the networklayer to send a TransportPacket to the signaled TPA.
	 * 
	 * @param packet
	 *            the TransportPacket that has to be sent
	 * @param to
	 *            the TPA to which the packet should be sent
	 * @throws IOException
	 */
	public void sendPacket(TransportPacket packet, TPA to,
			TransportConnection con) throws IOException {
		if (con.isConnected() || packet.isSyn()) {
			network.send(packet.toByteArray(), to);
		}

	}

	public void addNewApplication(TPA incoming, TPA outgoing, int inport,
			int outport, ControlConnection c) {

		TransportConnection newconn = new TransportConnection(incoming,
				outgoing, (byte) inport, (byte) outport, this, c);
		routingTable.add(newconn);
		newconn.run();
	}

	public Network getNetwork() {
		return this.network;
	}

	public ArrayList<TransportConnection> getConnections() {
		return connections;
	}

	public void run() {
		while (true) {
			try {
				s = serverSocket.accept();
				new ControlConnection(s, this).listen(Integer.toString(lastListened));
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	public TransportRoutingTable getTable() {
		return this.routingTable;
	}

	public void addListener(ListeningConnection l) {
		listeners.add(l);
	}

	public void addConnection(TransportConnection tc) {
		routingTable.add(tc);
	}

	@Override
	public void update(Observable network, Object arg) {
		NetworkPacket pack;
		while ((pack = ((Network) network).getPacket()) != null) {
			TransportPacket transPack = TransportPacket.fromByteArray(pack.getPayload());
			if (transPack.CheckChecksum()) {
				boolean found = false;
				TransportConnection conn = null;
				for (int i = 0; i < routingTable.getTable().size() && !found; i++){
					if (routingTable
							.getTable()
							.get(i)
							.isSameConnection((int) transPack.getSource(),
									(int) transPack.getDest(),
									pack.getFromTPA(), pack.getToTPA())) {
						conn = routingTable.getTable().get(i);
						found = true;
					}
				}
				if (!found && transPack.isSyn()) {
					for (int j = 0; j < listeners.size() && !found; j++) {
						if (listeners.get(j).isListening(transPack.getDest())) {
							listeners.get(j).makeConnection(
									transPack.getSource(), pack.getFromTPA());
							lastListened = listeners.get(j).getPort()+1;
									try {
										new ControlConnection(s,this).listen(Integer.toString(lastListened));
									} catch (IOException e) {
										// TODO Auto-generated catch block
										e.printStackTrace();
									}
							found = true;
							conn = listeners.get(j).getControl().getConnection();
							
						}
					}
				}
				try {
					conn.handle(transPack);
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}

			}else{
				System.out.println("Checksum TransportPacket klopt niet");
			}
		}

	}
}
